iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 9
0
Modern Web

關於 Ruby on Rails,我想說的是系列 第 19

[Day 19] 使用 ActiveJob & Sidekiq 背景執行工作

  • 分享至 

  • xImage
  •  

昨天ApplicationMailer實作了訂單成立後寄送email,不知道大家有沒有發現寄 email 這個工作花費的時間特別久,我的體感時間大概要三秒,如果讓客戶成立訂單後等三秒才能看到訂單完成頁,他應該下次就不想買了(´-ι_-`)
不只是寄送email,產生excel/csv檔,或是解析excel/csv檔...等,都是要耗費不少時間的工作,依照 Rails server 原來的設定,等一個工作結束後才能再做下一個,但是在生活節奏如此快的現代,這樣應該生意也不用做了。所以今天的主題背景執行工作就是必要的功能。當我們把一個工作移到背景去執行,server就不用等待這個工作執行完成才能做下一動 (又不是當兵),也就達到異步的效果。

今天的目標是把昨天的email寄送移到背景執行

新增工作

使用 Rails 的 ActiveJob 產生器g job ,工作命名為order_create_email
在Command line 執行:

bin/rails g job order_create_email 

產生了兩個檔案:

invoke  test_unit
create    test/jobs/order_create_email_job_test.rb
create  app/jobs/order_create_email_job.rb

直接來看app/jobs/order_create_email_job.rb

class OrderCreateEmailJob < ApplicationJob
  queue_as :default

  def perform(*args)
    # Do something later
  end
end

queue_as後面帶的參數是決定這個工作會不會很急,可以用這三個參數:

  • 預設值是 :default
  • 不急 :low_priority
  • 急件 :urgent

這個job到底要做什麼內容,是放在perform(*args)這個block內。我修改block內容為訂單成立後寄信:

class OrderCreateEmailJob < ApplicationJob
  queue_as :default

  def perform(order_id)
    products = Order.find(order_id).products
    AdminMailer.notify_customer(products).deliver_now 
  end
end

同時修改原本要立刻寄信的Order Model after_create

class Order < ApplicationRecord
  after_create { OrderCreateEmailJob.perform_later(self.id) }
  
  has_many :products
end

使用perform_later這個class_method,並把參數order_id帶入。到此就算設定完成了,可以用背景工作寄信囉。

另外,如果想要指定什麼時候做的話,可以這樣寫:

這樣是 5 秒之後做

OrderCreateEmailJob.set(wait: 2.seconds).perform_later(self.id)

這樣是「明天下午再做」

OrderCreateEmailJob.set(wait_until: Date.tomorrow.noon).perform_later(self.id)

執行背景工作

跟昨天一樣在console建立一筆訂單:

Order.create!(name: '#3', products: Product.all)

可以看到系統有job 的log:

Enqueued OrderCreateEmailJob (Job ID: c6131b30-6dc9-4609-ac5c-da70125adcad) to Async(default) with arguments: 14
Performing OrderCreateEmailJob (Job ID: c6131b30-6dc9-4609-ac5c-da70125adcad) from Async(default) with arguments: 14
...
...中間省略...
...
Performed OrderCreateEmailJob (Job ID: c6131b30-6dc9-4609-ac5c-da70125adcad) from Async(default) in 7360.53ms
Order.create!

大致上可以看到,Rails 先Enqueue(排進執行序)一個job,再Performing(開始執行),最後Performed(執行完成)。
每個job都有屬於自己的job-id,這個 job 的id是c6131b30-6dc9-4609-ac5c-da70125adcad
這樣我們就不用等到信寄出後才能執行下個請求,能夠讓工作分配更有效率。

使用Sidekiq

「排隊」(Queue)預設是會把排程放在記憶體裡,但如果萬一伺服器當機或重開機,這個排隊的資料就不見了。在實務上常會另外設置可以排隊的地方,常見的有 SidekiqDelayed Job,這裡介紹Sidekiq做排程。
因為Sidekiq使用Redis這個database store 儲存任務,而Redis透過key-value來儲存資料,Redis的多process執行,能夠同時執行多個任務。

安裝Redis

brew install redis

在Gemfile新增

gem 'redis', '~> 4.0'
gem 'redis-rails'

如果要使用Redis來cache 的資料,還要再安裝gem 'redis-namespace'gem 'redis-rack-cache'

安裝Sidekiq

在Gemfile新增

gem 'sidekiq'

啟動Sidekiq

我們需要啟動另外的sidekiq process來執行這些非同步的任務:

bundle exec sidekiq

預設的ActiveJob Adapter是:inline,也就是沒有非同步。要改一下 ActiveJob 內建存放工作的地方,請編輯 app/configs/application.rb

  module Todolist
    class Application < Rails::Application
      config.active_job.queue_adapter = :sidekiq
    end
  end

(Todolist 是這個Rails 專案的名字)

好了,前置作業完成。

讓我們再來執行一次訂單建立,並觸發after_create

一樣在console建立一筆訂單:

Order.create!(name: '#4', products: Product.all)

觀察console的log可以發現,在使用sidekiq之前信件內容/寄件人/收件人會跟SQL語法印在一起,使用Sidekiq後信件log跑到剛才執行bundle exec sidekiq的頁面:

可以看到最後兩行紀錄著剛才class=OrderCreateEmailJob還有jid=da9f2157ee5adb5faf45530a

這樣異步工作就算完成了。

Sidekiq本身的功能非常強大,可以透過Sidekiq::Status來確認工作的狀態是queued,working,failed,completed那一個,甚至可以知道現在進度是幾%了。如果再搭配sidekiq-scheduler,還可以做到預設工作時間及循環執行的功能,像我想要每個月的第一天計算上個月賺了多少錢,再把帳務明細寄出,這些都可以辦到。

ActiveJob 改用Sidekiq worker

以上是用 Rails 原生的ActiveJob來做背景工作,既然我們都用了Sidekiq,也可以改用Sidekiq內建的worker
首先在app底下新增workers資料夾,再新增 order_create_email_worker.rb這個檔案,把剛才放在app/jobs/order_create_email_job.rb的內容搬進來:

class OrderCreateEmailWorker
  include Sidekiq::Worker
  sidekiq_options retry: true
  
  def perform(order_id)
    products = Order.find(order_id).products
    AdminMailer.notify_customer(products).deliver_now
  end
end

第二行include Sidekiq::Workerworker的方法導入。
第三行retry: true,當worker失敗時,自動重新執行。

最後把原本呼叫ActiveJob的地方改成呼叫Sidekiq::Worker。
app/models/order.rb換成worker就可以了

class Order < ApplicationRecord
  after_create { OrderCreateEmailWorker.perform_async(id) }
  has_many :products
end

結語

用這張圖做結尾

Courtesy of https://www.youtube.com/watch?v=GBEDvF1_8B8

原本由Controller來分配工作,等一個工作執行完再跑下一個。現在透過Redis來Queue(排程)讓工作分頭進行,我們只要提示客戶工作已經排程處理,就可以放心讓 Rails 應用程式繼續執行其他任務。


上一篇
[Day 18] 使用 ApplicationMailer 寄Email
下一篇
# [Day 20] 多對多關聯及多型關聯
系列文
關於 Ruby on Rails,我想說的是23
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言